package com.github.utils;

import com.github.exceptions.TimeMatchFormatException;
import com.github.utils.expand.MethodType;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <h1> XML转换相关工具 </h1>
 * <p>
 * <h2>基于注解的相关解释</h2>
 * <p>注解：{@link javax.xml.bind.annotation.XmlAccessorType} 控制字段或属性的序列化。
 * FIELD表示JAXB将自动绑定Java类中的每个非静态的（static）、非瞬态的（由@XmlTransient标注）字段到XML。
 * 其他值还有XmlAccessType.PROPERTY和XmlAccessType.NONE</p>
 * <p>注解：{@link javax.xml.bind.annotation.XmlRootElement} {@code name = string}设置根节点</p>
 * <p>注解：{@link javax.xml.bind.annotation.XmlType} 将Java类或枚举类型映射到XML模式类型</p>
 * <p>注解：{@link javax.xml.bind.annotation.XmlElement} 将Java类的一个属性映射到与属性同名的一个XML元素</p>
 * <p>注解：{@link javax.xml.bind.annotation.XmlAttribute} 将Java类的一个属性映射到与属性同名的一个XML属性</p>
 * <p>注解：{@link javax.xml.bind.annotation.XmlElementWrapper} 对于数组或集合（即包含多个元素的成员变量），
 * 生成一个包装该数组或集合的XML元素（称为包装器）</p>
 * <p>
 *
 * @author  Crab2Die
 */
public class XMLUtils {

    static private Logger logger = Logger.getLogger(XMLUtils.class);

    static private String UTF8 = "UTF-8";
    static private String GBK = "GBK";

    //----------------------------非注解方式，基于dom4j的XML解析-------------------------------//
    //###################################################
    //  简单xml解析
    //###################################################

    /**
     * <p>根据XML文件路径加载XML文件至{@link org.dom4j.Document}</p>
     *
     * @param filePath 文件地址
     * @return {@link org.dom4j.Document} Document对象
     * @throws DocumentException
     * @author  Crab2Died
     */
    static
    public Document loadDoc(String filePath) throws DocumentException {
        Document doc;
        SAXReader reader = new SAXReader();
        reader.setValidation(false);
        reader.setEncoding(UTF8);
        //reader.setEntityResolver(new E);
        doc = reader.read(new File(filePath));
        return doc;
    }


    /**
     * <p>以指定编码格式返回xml</p>
     *
     * @param doc      Document对象
     * @param encoding 编码方式
     * @return {@link String}   XML
     */
    static
    public String doc2Str(Document doc, String encoding) {
        if (null != doc) {
            OutputFormat outputFormat = new OutputFormat();
            outputFormat.setEncoding(encoding);
            StringWriter stringWriter = new StringWriter();
            XMLWriter xmlWriter = new XMLWriter(stringWriter, outputFormat);
            try {
                xmlWriter.write(doc);
                return stringWriter.toString();
            } catch (IOException ex) {
                logger.error(ex);
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * <p>将Document对象转换为元素Map集合</p>
     *
     * @param doc dom4j的{@link org.dom4j.Document}对象
     * @return {@code Map<String, Element>}对象
     * @author  Crab2Died
     */
    private static Map<String, Element> doc2MapElement(Document doc) {
        if (null == doc)
            return null;
        Map<String, Element> map = new HashMap<>();
        Element root = doc.getRootElement();
        List children = root.elements();
        if (null != children && children.size() > 0) {
            for (Object child : children) {
                Element element = (Element) child;
                map.put(element.getName(), element);
            }
        }
        return map;
    }

    /**
     * <p>XML文件内容转java对象</p>
     *
     * @param xmlFilePath XML文件路径
     * @param clazz       目标对象
     * @return T               目标对象
     * @throws DocumentException,IllegalAccessException,InstantiationException,
     * @throws ParseException,TimeMatchFormatException,
     * @throws NoSuchMethodException,InvocationTargetException
     * @author  Crab2Died
     */
    static
    public <T> T parseNoRelyByFile(String xmlFilePath, Class<T> clazz) throws DocumentException,
            IllegalAccessException, InstantiationException, ParseException, TimeMatchFormatException,
            NoSuchMethodException, InvocationTargetException {

        Document doc = loadDoc(xmlFilePath);
        return parse(doc, clazz);
    }

    /**
     * <p>将XML内容转为java对象</p>
     *
     * @param xml   xml内容
     * @param clazz 目标对象
     * @return T         返回对象
     * @throws DocumentException,IllegalAccessException,InvocationTargetException
     * @throws ParseException,InstantiationException,TimeMatchFormatException,NoSuchMethodException
     * @author  Crab2Died
     */
    static
    public <T> T parseNoRelyByStr(String xml, Class<T> clazz) throws DocumentException,
            IllegalAccessException, InvocationTargetException, ParseException,
            InstantiationException, TimeMatchFormatException, NoSuchMethodException {

        if (StringUtils.isEmpty(xml))
            return null;
        return parse(DocumentHelper.parseText(xml), clazz);
    }

    /**
     * <p></p>
     *
     * @param doc   XML文档对象
     * @param clazz 目标对象
     * @return T         返回对象
     * @throws IllegalAccessException,InstantiationException,NoSuchMethodException,ParseException
     * @throws TimeMatchFormatException,InvocationTargetException
     * @author  Crab2Died
     */
    static
    private <T> T parse(Document doc, Class<T> clazz) throws IllegalAccessException, InstantiationException,
            NoSuchMethodException, ParseException, TimeMatchFormatException, InvocationTargetException {

        Map<String, Element> map = doc2MapElement(doc);
        T obj = clazz.newInstance();
        if (null != map && map.size() > 0) {
            for (Map.Entry<String, Element> entry : map.entrySet()) {
                String propertyName = entry.getKey();
                String value = entry.getValue().getTextTrim();
                Field field = ReflexUtils.matchClassField(clazz, propertyName);
                if (null == field) continue;
                String setter = ReflexUtils.getOrSet(field, MethodType.SET);
                clazz.getMethod(setter, field.getType()).invoke(obj, ReflexUtils.converType(value, field.getType()));
            }
        }
        return obj;
    }

    // --------------------------------基于注解解析XML------------------------------//

    /**
     * <p>基于注解，XML转java对象</p>
     *
     * @param clazz 目标对象
     * @param xml   xml对象
     * @return T    返回对象
     * @throws JAXBException
     * @author  Crab2Die
     */
    @SuppressWarnings("unchecked")
    static
    public <T> T parse(String xml, Class<T> clazz) throws JAXBException {

        JAXBContext context = JAXBContext.newInstance(clazz);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        StringReader sr = new StringReader(xml);
        return (T) unmarshaller.unmarshal(sr);
    }

    /**
     * <p>基于注解，XML转java对象</p>
     *
     * @param clazz   目标对象
     * @param xmlPath xml文件地址
     * @return T      返回对象
     * @throws JAXBException
     * @author  Crab2Die
     */
    @SuppressWarnings("unchecked")
    static
    public <T> T parseByFile(String xmlPath, Class<T> clazz) throws JAXBException,
            FileNotFoundException {

        JAXBContext context = JAXBContext.newInstance(clazz);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        FileReader fileReader = new FileReader(xmlPath);
        return (T) unmarshaller.unmarshal(fileReader);
    }

    /**
     * <p>基于注解，java对象转XML</p>
     *
     * @param t 转换对象
     * @return {@link String}   XML
     * @throws JAXBException
     * @author  Crab2Die
     */
    static
    public String toXml(Object t) throws JAXBException {

        StringWriter sw = new StringWriter();
        JAXBContext context = JAXBContext.newInstance(t.getClass());
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.setProperty(Marshaller.JAXB_ENCODING, UTF8);
        marshaller.marshal(t, sw);
        return sw.toString();
    }

    /**
     * <p>基于注解，java对象转XML</p>
     *
     * @param t        转换对象
     * @param filePath 输出xml路径
     * @throws JAXBException
     * @author  Crab2Die
     */
    static
    public void toXml(Object t, String filePath) throws JAXBException,
            IOException {

        FileWriter fw = new FileWriter(filePath);
        JAXBContext context = JAXBContext.newInstance(t.getClass());
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.setProperty(Marshaller.JAXB_ENCODING, UTF8);
        marshaller.marshal(t, fw);
    }

}
